/* -+- file  ./kf2/GUI/RosterView.cs -+- */
/*
 * kf2 jabber client
 * ~~~~~~~~~~~~~~~~~
 *
 * Copyright (C) 2006-2007 Przemysław Sitek
 * 				<przemyslaw.sitek at wp.pl> 
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using Mono.Unix;
using Habazie.kf2.Core;
using Habazie.kf2.Net;
using Habazie.kf2.Roster;
using Habazie.kf2.Exposition;

namespace Habazie.kf2.GUI
{

	///<summary>A widget for displaying rosters</summary>
	public class RosterView : UltraTreeView
	{
		#region Private fields
		
		///<summary>TreeStore for storing roster</summary>
		private Gtk.TreeStore m_Store; // TODO: isn't it redundant?
		private Gtk.TreeModelFilter filter;
		private Gtk.TreeModelSort sort;
		
		///<summary>Display offline contacts</summary>
		private bool m_DisplayOffline = false;
		
//		private bool m_displayStatus = true;
		
		///<summary>ConnectionManager to get rosters from</summary>
		private ConnectionManager m_Connections = null;

		///<summary>Columns in roster view</summary>	
		enum Columns {
			Item = 0,
			Pix1 = 1,
			Text = 2,
			Pix2 = 3,
			PixVisible = 4,
			ItemVisible = 5
		}
		#endregion
		#region Constructors
		
		public RosterView() : base ()
		{
			m_Store = new Gtk.TreeStore (typeof (IRosterItem),	// roster item
									typeof (Gdk.Pixbuf),	// roster icon
									typeof (string),		// item name
									typeof (Gdk.Pixbuf),	// additional icon
									typeof (bool),			// roster icon visible
									typeof (bool)			// whether to show item
							);
			
			/* Add filter */
			filter = new Gtk.TreeModelFilter (m_Store, null);
			filter.VisibleFunc = FilterFunc;
		//	filter.VisibleColumn = (int) Columns.ItemVisible;
			
			/* Sort model */
			sort = new Gtk.TreeModelSort (filter);
			sort.DefaultSortFunc = SortFunc;
			
			
			Model = sort;
			Gtk.TreeViewColumn col = new Gtk.TreeViewColumn ();
			
			Gtk.CellRendererPixbuf pix = new Gtk.CellRendererPixbuf ();
			col.PackStart (pix, false);
			col.AddAttribute (pix, "pixbuf", 1);
			col.AddAttribute (pix, "visible", 4);
			
			Gtk.CellRendererText text = new Gtk.CellRendererText ();
			text.Ellipsize = Pango.EllipsizeMode.End;
			col.PackStart (text, true);
			col.AddAttribute (text, "markup", 2);
			
			Gtk.CellRendererPixbuf pix2 = new Gtk.CellRendererPixbuf ();
			col.PackEnd(pix2, false);
			col.AddAttribute (pix2, "pixbuf", 3);
			
			AppendColumn (col);
		}
		
		#endregion
		#region Properties
		
		///<summary>Connections to be displayed</summary>
		public ConnectionManager Connections {
			set {
				m_Connections = value;
				m_Connections.Changed += OnConnectionsChanged;
				
				/* Do sth with rosters */
				foreach (Account acc in m_Connections.Accounts) {
					Roster.Roster r = acc.GetRosterRef ();
					acc.Changed += AccountChanged;
					acc.PasswordNeeded += PromptForPassword;
					r.RosterUpdated += xUpdateRoster;
					r.RosterItemUpdated += xUpdateRosterItem;
				}

				m_Connections.AccountAdded += delegate (object o, AccountEventArgs args) {
					Account acc = args.Account;
					Roster.Roster r = acc.GetRosterRef ();
					acc.Changed += AccountChanged;
					acc.PasswordNeeded += PromptForPassword;
					r.RosterUpdated += xUpdateRoster;
					r.RosterItemUpdated += xUpdateRosterItem;
				};

				// TODO: handle deletes
			}
		}
		
		public bool DisplayOffline {
			get { return m_DisplayOffline; }
			set {
				m_DisplayOffline = value;
				filter.Refilter ();
			}
		}
		
		#endregion
		#region Public methods
		
		public void Update ()
		{
			UpdateViewSingle ();
		}
		
		///<summary>Update contents of a roster</summary>
		///<remarks>This implementation does not show groups</remarks>
		public void UpdateViewMulti ()
		{
			m_Store.Clear ();
			foreach (Account acc in m_Connections.Accounts) {
				Gtk.TreeIter aiter = new Gtk.TreeIter ();
				m_Store.Append (out aiter);
				m_Store.SetValue (aiter, 0, acc);
				UpdateIter (aiter);
			//	acc.DisplayAtIter (m_Store, aiter);
				
				Roster.Roster r = acc.GetRosterRef ();
			//	Console.WriteLine ("roster={0}", r);
					
				foreach (Item item in r) {
					Gtk.TreeIter iter = m_Store.Append (aiter);
					m_Store.SetValue (iter, 0, item);
					UpdateIter (iter);
				//	item.DisplayAtIter (m_Store, iter);
				}
			}
		}
		
		
		
		protected virtual void UpdateIter (Gtk.TreeIter iter)
		{
			// FIXME: this needs reworking
			object o = m_Store.GetValue (iter, 0);
			//m_Store.SetValue (iter, 0, o);
			if (o is Account) {
				Account a = (Account) o;
				Gdk.Pixbuf pix;
				if (a.SSLon)
					pix = Gdk.Pixbuf.LoadFromResource ("lock.png");
				else
					pix = Gdk.Pixbuf.LoadFromResource ("unlock.png");
				
				Gdk.Pixbuf spix;
				if (a.NetworkState == AccountState.Connecting)
					spix = Pixbufs.Image (Pixbuf.Hourglass);
				else
					spix = Pixbufs.Get (a.StatusType, a.StatusShow);
					
				if (spix != null)
					m_Store.SetValue (iter, 1, spix);

//				string status;
//				if (a.StatusText != null)
//					status = Util.EscapeString (a.StatusText);
//				else
//					status = Util.StatusToString (a.StatusType, a.StatusShow);
//
				m_Store.SetValue (iter, 4, true);	
				m_Store.SetValue (iter, 2, String.Format ("{0}\n<span size=\"x-small\" foreground=\"#707070\">{1}</span>", a.Name, a.Description));
				m_Store.SetValue (iter, 3, pix);	
				m_Store.SetValue (iter, (int) Columns.ItemVisible, true);	
			} else if (o is Item) {
				Item i = (Item) o;
				m_Store.SetValue (iter, 4, true);	
				
				if (i.Resource != null) {
					m_Store.SetValue (iter, 1, Pixbufs.Get (i.Resource.Presence, i.Resource.Show));
				}
				string s;
				if (i.Name == null || i.Name == "")
					s = i.JabberID.ToString ();
				else
					s = i.Name;
		
				string status;
				if (i.Resource != null) { // FIXME: Resource r = i.Resource;
					m_Store.SetValue (iter, 4, true);	
					m_Store.SetValue (iter, 1, Pixbufs.Get (i.Resource.Presence, i.Resource.Show));
					if (i.Resource.Status != null)
						status = Util.EscapeString (i.Resource.ShortStatus);
					else
						status = Util.StatusToString (i.Resource.Presence, i.Resource.Show);
					
				} else {
					status = Catalog.GetString ("Disconnected");
				}
				m_Store.SetValue (iter, 2, String.Format ("{0}\n<span size=\"x-small\" foreground=\"#707070\">{1}</span>",s,status));
				m_Store.SetValue (iter, (int) Columns.ItemVisible, true);	
			} else if (o is RosterGroup) {
				RosterGroup g = (RosterGroup) o;
				m_Store.SetValue (iter, 2, String.Format ("<b>{0}</b> ({1}/{2})", g.Name, g.Visible, g.Total));
				m_Store.SetValue (iter, 4, false);	
				m_Store.SetValue (iter, (int) Columns.ItemVisible, true);	
			}
		}
		
		
		
		protected virtual void OnConnectionsChanged (object sender, EventArgs args)
		{
			Gtk.Application.Invoke (delegate {	// Is this required?
				Update ();
			});
		}
		
		protected virtual void AccountChanged (object sender, EventArgs args)
		{
			Gtk.Application.Invoke (delegate {
				Update ();
			});
		}
		
		protected virtual void xUpdateRoster (object sender, EventArgs args)
		{
			Util.Debug ("gui", "Updating roster");
			Gtk.Application.Invoke (delegate {
				Update ();
			});
		}
		
		protected virtual void xUpdateRosterItem (object sender, RosterItemUpdatedEventArgs args)
		{
			Util.Debug ("gui", "Updating roster items");
			Gtk.Application.Invoke (delegate {
				UpdateItem (args.UpdatedItem);
			});
		}
	
		#endregion
		#region Merged roster view
		
		LinkedList<RosterGroup> m_Groups = new LinkedList<Habazie.kf2.GUI.RosterView.RosterGroup> ();
		
		internal class RosterGroup : IEnumerable {
			LinkedList<Item> m_items;
			string name = null;
			private int m_visible = 0;
			private bool m_special;
			
			public RosterGroup (string n)
			{
				m_items = new LinkedList<Item> ();
				name = n;
			}
			
			public string Name {
				get { return name; }
			}
			
			
			public int Total {
				get { return m_items.Count; }
			}
			
			public int Visible {
				get { return m_visible; }
			}
			
			public bool Special {
				get { return m_special; }
				set { m_special = value; /* FIXME */ }
			}
			
			public void Add (Item i)
			{
				m_items.AddLast (i);
			}
			
			public IEnumerator GetEnumerator () {
				return m_items.GetEnumerator ();
			}
			
			
			///<summary>
			///	Update group meta information
			///</summary>
			public void Update ()
			{
				m_visible = 0;
				foreach (Item i in m_items)
					if (i.Available)
						m_visible++;
			}
		}
		
		
		///<summary>
		///	Get roster group
		///</summary>
		private RosterGroup GetGroup (string name) {
			foreach (RosterGroup g in m_Groups)
				if (g.Name == name)
					return g;
			RosterGroup ga = new RosterGroup (name);
			m_Groups.AddLast (ga);
			return ga;
		}
		
		
		///<summary>
		///	Update one item information
		///</summary>
		public void UpdateItem (Item item)
		{
			Gtk.TreeIter group;
			
			if (item == null)
				return;
			
			// Update groups information
			foreach (string groupname in item.Groups) {
				RosterGroup g = GetGroup (groupname);
				if (g != null)
					g.Update ();
			}
			
			// Update list
			if (m_Store.GetIterFirst (out group)) {
				do {
					Gtk.TreeIter iter;
					bool changed = false;	// item in this group has been changed		
				
					if (m_Store.IterChildren (out iter, group)) {
						do {
							try {
								Item myItem = (m_Store.GetValue (iter, 0) as Item);
								if (myItem == item) {
									UpdateIter (iter);
									changed = true;
								}
							} catch (Exception e) {
								Util.ReportException (e);
							}
						} while (m_Store.IterNext (ref iter));
					}

					// Item in this group has been changed - update group as well
					if (changed)
						UpdateIter (group);
				} while (m_Store.IterNext (ref group));
			}
			
			filter.Refilter ();
		}
		
		
		public void UpdateViewSingle ()
		{
			// FIXME: What about items that do not belong to any group?
			Util.Debug ("gui", "UpdateViewSingle ()");
			/* Prevent mass updating of view */
			this.Model = null;
			
			m_Groups.Clear ();
			m_Store.Clear ();
			
			
			foreach (Account a in m_Connections.Accounts) {
				foreach (Item i in a.GetRosterRef ()) {
					if (i.GroupsCount == 0) {
						RosterGroup g = GetGroup ("General");
						g.Add (i);
					} else {
						foreach (string n in i.Groups) {
							RosterGroup g = GetGroup (n);
							g.Add (i);
						}
					}
				}
			}
			
			foreach (RosterGroup g in m_Groups) {
				g.Update ();
				Gtk.TreeIter groupIter = new Gtk.TreeIter ();
				m_Store.Append (out groupIter);
				m_Store.SetValue (groupIter, 0, g);
				UpdateIter (groupIter);
				
				foreach (Item i in g) {
					Gtk.TreeIter iter = m_Store.Append (groupIter);
					m_Store.SetValue (iter, 0, i);
					UpdateIter (iter);
				}
			}
			
			/* Display accounts in roster */
			RosterGroup accountsGroup = GetGroup ("Accounts");
			accountsGroup.Special = true;
			Gtk.TreeIter accountsIter = new Gtk.TreeIter ();
			m_Store.Append (out accountsIter);
			m_Store.SetValue (accountsIter, 0, accountsGroup);
			UpdateIter (accountsIter);

			foreach (Account a in m_Connections.Accounts) {
				Gtk.TreeIter groupIter = m_Store.Append (accountsIter);
				m_Store.SetValue (groupIter, 0, a);
				UpdateIter (groupIter);
			}
			
			/* Show model */
			this.Model = sort;
			this.ExpandAll ();
		}
		
		#endregion
		
		protected virtual int SortFunc (Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b)
		{
			string av = (string) model.GetValue (a, (int) Columns.Text);
			string bv = (string) model.GetValue (b, (int) Columns.Text);
			if (av != null && av != null)
				return av.CompareTo (bv);
			else
				return 0;
		}
		
		
		protected virtual bool FilterFunc (Gtk.TreeModel model, Gtk.TreeIter iter)
		{
			if (m_DisplayOffline)
				return true;
			
			object o = model.GetValue (iter, (int) Columns.Item);
			Item i; RosterGroup g;
			
			if ((i = o as Item) != null) {
				return i.Available || i.InGroup ("Hidden");
			} else if ((g = o as RosterGroup) != null) {
				return g.Special || g.Visible > 0;
			}
		//	string av = (string) model.GetValue (iter, (int) Columns.Text);
		//	bool visible = (bool) model.GetValue (iter, (int) Columns.ItemVisible);
		//	Util.Debug ("gui", "Filtering '{0}' = {1}", av, visible);
			return true;
		}
		
		#region Protected event handlers
		
		
		protected virtual void PromptForPassword (object o, PasswordEventArgs args)
		{
			Account account = o as Account;
			
			Gtk.Dialog d = new Gtk.Dialog ("Enter password",
			                               this.Toplevel as Gtk.Window,
			                               Gtk.DialogFlags.DestroyWithParent | Gtk.DialogFlags.NoSeparator);
			Gtk.HBox hbox = new Gtk.HBox (false, 12);
			hbox.BorderWidth = 12;
			Gtk.Image img = new Gtk.Image ("gtk-dialog-authentication", Gtk.IconSize.Dialog);
			hbox.PackStart (img, false, false, 0);
			Gtk.VBox vbox = new Gtk.VBox (false, 6);
			Gtk.Label l = new Gtk.Label ();
			l.LineWrap = true;
		//	l.MaxWidthChars = 16;
			l.Markup = String.Format ("Enter password for\n<b>{0}</b> &lt;{1}@{2}&gt;",
			                          GLib.Markup.EscapeText (account.Name),
			                          account.User, account.Server);
			vbox.PackStart (l, false, false, 0);
			                          
			Gtk.Entry e = new Gtk.Entry ();
			e.Visibility = false;
			vbox.PackStart (e, false, false, 0);
			hbox.PackStart (vbox, true, true, 0);
			d.VBox.PackStart (hbox, true, true, 0);
			d.AddButton (Gtk.Stock.Cancel, Gtk.ResponseType.Cancel);
			d.AddButton (Gtk.Stock.Ok, Gtk.ResponseType.Ok);
			d.Resizable = false;
			d.ShowAll ();
			int response = d.Run ();
			string pass = e.Text;
			d.Destroy ();
			if (response == (int) Gtk.ResponseType.Ok)
				args.Password = pass;
		}
		
		///<summary>
		///	Get iter at specified position
		///</summary>
//		private Gtk.TreeIter GetIter (int x, int y)
//		{
//			Gtk.TreePath path;
//			if (GetPathAtPos (x, y, out path)) {
//				Gtk.TreeIter iter;
//				if (this.sort.GetIter (out iter, path)) {
//	//				string text = (string)
//	//					sort.GetValue (iter, (int) Columns.Text);
//					return iter;
//				}
//			}
//			return Gtk.TreeIter.Zero;
//		}
//		
		protected override bool OnButtonPressEvent (Gdk.EventButton evnt)
		{
			IRosterItem item = null;
			Gtk.TreeIter iter;
			m_Store.IterChildren (out iter);
	
			iter = GetIter ((int)evnt.X, (int)evnt.Y);

			try {
				item = (IRosterItem) sort.GetValue (iter, (int) Columns.Item);
			} catch (Exception e) {
				Util.ReportException (e);
			}
				
			if (evnt.Type == Gdk.EventType.TwoButtonPress) {
				if (item is Habazie.kf2.Roster.Item) {
					Chat win = Chat.Get (item as Habazie.kf2.Roster.Item);
					win.Show ();
					win.Raise ();
				} else if (item is Habazie.kf2.Net.Account) {
					Account a = (Account) item;
					XmlConsole win = new XmlConsole (a);
					win.Show ();
				}
				return true;
			} else if (evnt.Button == 3) {
				if (item is Habazie.kf2.Roster.Item) {
					 ItemMenu menu = new ItemMenu ((Habazie.kf2.Roster.Item) item);
					 menu.ActionPerformed += MenuSelected;
					 menu.Popup ();
				} else if (item is Account) {
					Gtk.Menu m = new AccountMenu ((Account)item);
					m.Popup ();
				}
				return false;
			}
			return base.OnButtonPressEvent (evnt);
		}
		
		protected virtual void MenuSelected (object o, ActionEventArgs args)
		{
			switch (args.Action) {
				case ActionType.Remove:		/* Remove a roster item from roster */
					{
						Gtk.MessageDialog win = new Gtk.MessageDialog (this.Toplevel as Gtk.Window,
								Gtk.DialogFlags.Modal,
								Gtk.MessageType.Question,
								Gtk.ButtonsType.YesNo,
								true,
								Catalog.GetString ("<big><b>Question</b></big>\n\nDo you want to remove <b>{0}</b> <i>&lt;{1}&gt;</i> from your roster?"),
								args.Item.Name, args.Item.JabberID.ToString ());
						win.Title = Catalog.GetString ("Question");
			//			Gtk.CheckButton box = new Gtk.CheckButton ("Unsubscribe");
			//			box.Show ();
			//			win.VBox.PackStart (box, false, false, 0);
						int response = win.Run ();
						win.Destroy ();
						if (response == (int) Gtk.ResponseType.Yes) {
							args.Item.Account.GetRosterRef ().RequestRemoveItem  (args.Item.JabberID, true);
						}
					}
					break;
				case ActionType.GroupAdd:
					{
						string name = Reveal.Prompt (this.Toplevel as Gtk.Window,
							Catalog.GetString ("New group name:"), Catalog.GetString ("New Group"));
						if (name != null)
							args.Item.Account.GetRosterRef ().RequestSetGroup (args.Item, name);
					}
					break;
				case ActionType.GroupEdit:
					break;
				case ActionType.GroupClear:
					args.Item.Account.GetRosterRef ().RequestSetGroup (args.Item, null);
					break;
				case ActionType.GroupSet:
					args.Item.Account.GetRosterRef ().RequestSetGroup (args.Item, args.Text);
					break;
				case ActionType.Rename:
					{
						string name = Reveal.Prompt (null, Catalog.GetString ("New item name:"), args.Item.Name);
						if (name != null)
							args.Item.Account.GetRosterRef ().RequestRename (args.Item, name);
					}
					break;
			}
		}
		
		#endregion
		#region Cool tooltip


		/// <summary>
		/// 	Setup tooltip contents
		/// </summary>
		protected override bool QueryTooltip (Gtk.TreeIter iter, out string text, out string desc, out Gdk.Pixbuf pix)
		{

			try {
				IRosterItem item = (IRosterItem) sort.GetValue (iter, (int) Columns.Item);
				Item i;
				if ((i = item as Item) != null) {
					text = i.Name;
					desc = TooltipText (i);
					pix = i.Avatar.Pixbuf;
					return true;
				}
			} catch (Exception e) {
//				Util.ReportException (e);
			}

			return base.QueryTooltip (iter, out text, out desc, out pix);
		}
		
		
		///	<summary>
		/// 	Prepare tool tip text contents
		/// </summary>
		protected virtual string TooltipText (Item i)
		{
			System.Text.StringBuilder b = new System.Text.StringBuilder ();
			b.AppendFormat ("<b>{0}:</b> {1}\n", Catalog.GetString ("JID"), i.JabberID);
			b.AppendFormat ("<b>{0}:</b> {1}\n", Catalog.GetString ("Subscription"), i.Sub.ToString ());
			b.AppendFormat ("\n<u>{0}</u>", Catalog.GetString ("Status"));
			foreach (Resource r in i.Resources) {
				if (r.Status != null)
					b.AppendFormat ("\n<b>{0}:</b> {1}\n{2}", r.Name,
					Util.StatusToString (r.Presence, r.Show),
					Util.EscapeString (r.Status));
				else
					b.AppendFormat ("\n<b>{0}:</b> {1}", r.Name, Util.StatusToString (r.Presence, r.Show)); 
			}
			return b.ToString ();
		}
		
		#endregion
	}
}
